{
  system ? builtins.currentSystem,
  config ? { },
  pkgs ? import ../.. { inherit system config; },
  debug ? false,
  enableUnfree ? false,
  enableKvm ? false,
  use64bitGuest ? true,
}:

with import ../lib/testing-python.nix { inherit system pkgs; };
with pkgs.lib;

let
  testVMConfig =
    vmName: attrs:
    {
      config,
      pkgs,
      lib,
      ...
    }:
    let
      guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions;

      miniInit = ''
        #!${pkgs.runtimeShell} -xe
        export PATH="${
          lib.makeBinPath [
            pkgs.coreutils
            pkgs.util-linux
          ]
        }"

        mkdir -p /run/dbus /var
        ln -s /run /var
        cat > /etc/passwd <<EOF
        root:x:0:0::/root:/bin/false
        messagebus:x:1:1::/run/dbus:/bin/false
        EOF
        cat > /etc/group <<EOF
        root:x:0:
        messagebus:x:1:
        EOF

        "${pkgs.dbus}/bin/dbus-daemon" --fork \
          --config-file="${pkgs.dbus}/share/dbus-1/system.conf"

        ${guestAdditions}/bin/VBoxService
        ${(attrs.vmScript or (const "")) pkgs}

        i=0
        while [ ! -e /mnt-root/shutdown ]; do
          sleep 10
          i=$(($i + 10))
          [ $i -le 120 ] || fail
        done

        rm -f /mnt-root/boot-done /mnt-root/shutdown
      '';
    in
    {
      boot.kernelParams = [
        "console=tty0"
        "console=ttyS0"
        "ignore_loglevel"
        "boot.trace"
        "panic=1"
        "boot.panic_on_fail"
        "init=${pkgs.writeScript "mini-init.sh" miniInit}"
      ];

      fileSystems."/" = {
        device = "vboxshare";
        fsType = "vboxsf";
      };

      virtualisation.virtualbox.guest.enable = true;

      boot.initrd.kernelModules = [
        "af_packet"
        "vboxsf"
        "virtio"
        "virtio_pci"
        "virtio_ring"
        "virtio_net"
        "vboxguest"
      ];

      boot.initrd.extraUtilsCommands = ''
        copy_bin_and_libs "${guestAdditions}/bin/mount.vboxsf"
        copy_bin_and_libs "${pkgs.util-linux}/bin/unshare"
        ${(attrs.extraUtilsCommands or (const "")) pkgs}
      '';

      boot.initrd.postMountCommands = ''
        touch /mnt-root/boot-done
        hostname "${vmName}"
        mkdir -p /nix/store
        unshare -m ${escapeShellArg pkgs.runtimeShell} -c '
          mount -t vboxsf nixstore /nix/store
          exec "$stage2Init"
        '
        poweroff -f
      '';

      system.requiredKernelConfig = with config.lib.kernelConfig; [
        (isYes "SERIAL_8250_CONSOLE")
        (isYes "SERIAL_8250")
      ];

      networking.usePredictableInterfaceNames = false;
    };

  mkLog =
    logfile: tag:
    let
      rotated = map (i: "${logfile}.${toString i}") (range 1 9);
      all = concatMapStringsSep " " (f: "\"${f}\"") ([ logfile ] ++ rotated);
      logcmd = "tail -F ${all} 2> /dev/null | logger -t \"${tag}\"";
    in
    if debug then "machine.execute(ru('${logcmd} & disown'))" else "pass";

  testVM =
    vmName: vmScript:
    let
      cfg =
        (import ../lib/eval-config.nix {
          system = if use64bitGuest then "x86_64-linux" else "i686-linux";
          modules = [
            (testVMConfig vmName vmScript)
          ];
        }).config;
    in
    pkgs.vmTools.runInLinuxVM (
      pkgs.runCommand "virtualbox-image"
        {
          preVM = ''
            mkdir -p "$out"
            diskImage="$(pwd)/qimage"
            ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M
          '';

          postVM = ''
            echo "creating VirtualBox disk image..."
            ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \
              "$diskImage" "$out/disk.vdi"
          '';

          buildInputs = [
            pkgs.util-linux
            pkgs.perl
          ];
        }
        ''
          ${pkgs.parted}/sbin/parted --script /dev/vda mklabel msdos
          ${pkgs.parted}/sbin/parted --script /dev/vda -- mkpart primary ext2 1M -1s
          ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1
          ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1
          mkdir /mnt
          mount /dev/vda1 /mnt
          cp "${cfg.system.build.kernel}/bzImage" /mnt/linux
          cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd

          ${pkgs.grub2}/bin/grub-install --target=i386-pc --boot-directory=/mnt /dev/vda

          cat > /mnt/grub/grub.cfg <<GRUB
          set root=hd0,1
          linux /linux ${concatStringsSep " " cfg.boot.kernelParams}
          initrd /initrd
          boot
          GRUB
          umount /mnt
        ''
    );

  createVM =
    name: attrs:
    let
      mkFlags = concatStringsSep " ";

      sharePath = "/home/alice/vboxshare-${name}";

      createFlags = mkFlags [
        "--ostype ${if use64bitGuest then "Linux26_64" else "Linux26"}"
        "--register"
      ];

      vmFlags = mkFlags (
        [
          "--uart1 0x3F8 4"
          "--uartmode1 client /run/virtualbox-log-${name}.sock"
          "--memory 768"
          "--audio none"
        ]
        ++ (attrs.vmFlags or [ ])
      );

      controllerFlags = mkFlags [
        "--name SATA"
        "--add sata"
        "--bootable on"
        "--hostiocache on"
      ];

      diskFlags = mkFlags [
        "--storagectl SATA"
        "--port 0"
        "--device 0"
        "--type hdd"
        "--mtype immutable"
        "--medium ${testVM name attrs}/disk.vdi"
      ];

      sharedFlags = mkFlags [
        "--name vboxshare"
        "--hostpath ${sharePath}"
      ];

      nixstoreFlags = mkFlags [
        "--name nixstore"
        "--hostpath /nix/store"
        "--readonly"
      ];
    in
    {
      machine = {
        systemd.sockets."vboxtestlog-${name}" = {
          description = "VirtualBox Test Machine Log Socket For ${name}";
          wantedBy = [ "sockets.target" ];
          before = [ "multi-user.target" ];
          socketConfig.ListenStream = "/run/virtualbox-log-${name}.sock";
          socketConfig.Accept = true;
        };

        systemd.services."vboxtestlog-${name}@" = {
          description = "VirtualBox Test Machine Log For ${name}";
          serviceConfig.StandardInput = "socket";
          serviceConfig.StandardOutput = "journal";
          serviceConfig.SyslogIdentifier = "GUEST-${name}";
          serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
        };
      };

      testSubs = ''


        ${name}_sharepath = "${sharePath}"


        def check_running_${name}():
            cmd = "VBoxManage list runningvms | grep -q '^\"${name}\"'"
            (status, _) = machine.execute(ru(cmd))
            return status == 0


        def cleanup_${name}():
            if check_running_${name}():
                machine.execute(ru("VBoxManage controlvm ${name} poweroff"))
            machine.succeed("rm -rf ${sharePath}")
            machine.succeed("mkdir -p ${sharePath}")
            machine.succeed("chown alice:users ${sharePath}")


        def create_vm_${name}():
            cleanup_${name}()
            vbm("createvm --name ${name} ${createFlags}")
            vbm("modifyvm ${name} ${vmFlags}")
            vbm("setextradata ${name} VBoxInternal/PDM/HaltOnReset 1")
            vbm("storagectl ${name} ${controllerFlags}")
            vbm("storageattach ${name} ${diskFlags}")
            vbm("sharedfolder add ${name} ${sharedFlags}")
            vbm("sharedfolder add ${name} ${nixstoreFlags}")

            ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"}


        def destroy_vm_${name}():
            cleanup_${name}()
            vbm("unregistervm ${name} --delete")


        def wait_for_vm_boot_${name}():
            machine.execute(
                ru(
                    "set -e; i=0; "
                    "while ! test -e ${sharePath}/boot-done; do "
                    "sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; "
                    "VBoxManage list runningvms | grep -q '^\"${name}\"'; "
                    "done"
                )
            )


        def wait_for_ip_${name}(interface):
            property = f"/VirtualBox/GuestInfo/Net/{interface}/V4/IP"
            getip = f"VBoxManage guestproperty get ${name} {property} | sed -n -e 's/^Value: //p'"

            ip = machine.succeed(
                ru(
                    "for i in $(seq 1000); do "
                    f'if ipaddr="$({getip})" && [ -n "$ipaddr" ]; then '
                    'echo "$ipaddr"; exit 0; '
                    "fi; "
                    "sleep 1; "
                    "done; "
                    "echo 'Could not get IPv4 address for ${name}!' >&2; "
                    "exit 1"
                )
            ).strip()
            return ip


        def wait_for_startup_${name}(nudge=lambda: None):
            for _ in range(0, 130, 10):
                machine.sleep(10)
                if check_running_${name}():
                    return
                nudge()
            raise Exception("VirtualBox VM didn't start up within 2 minutes")


        def wait_for_shutdown_${name}():
            for _ in range(0, 130, 10):
                machine.sleep(10)
                if not check_running_${name}():
                    return
            raise Exception("VirtualBox VM didn't shut down within 2 minutes")


        def shutdown_vm_${name}():
            machine.succeed(ru("touch ${sharePath}/shutdown"))
            machine.execute(
                "set -e; i=0; "
                "while test -e ${sharePath}/shutdown "
                "        -o -e ${sharePath}/boot-done; do "
                "sleep 1; i=$(($i + 1)); [ $i -le 3600 ]; "
                "done"
            )
            wait_for_shutdown_${name}()
      '';
    };

  hostonlyVMFlags = [
    "--nictype1 virtio"
    "--nictype2 virtio"
    "--nic2 hostonly"
    "--hostonlyadapter2 vboxnet0"
  ];

  # The VirtualBox Oracle Extension Pack lets you use USB 3.0 (xHCI).
  enableExtensionPackVMFlags = [
    "--usbxhci on"
  ];

  dhcpScript = pkgs: ''
    ${pkgs.dhcpcd}/bin/dhcpcd eth0 eth1

    otherIP="$(${pkgs.netcat}/bin/nc -l 1234 || :)"
    ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP"
    echo "$otherIP reachable" | ${pkgs.netcat}/bin/nc -l 5678 || :
  '';

  sysdDetectVirt = pkgs: ''
    ${pkgs.systemd}/bin/systemd-detect-virt > /mnt-root/result
  '';

  vboxVMs = mapAttrs createVM {
    simple = { };

    detectvirt.vmScript = sysdDetectVirt;

    test1.vmFlags = hostonlyVMFlags;
    test1.vmScript = dhcpScript;

    test2.vmFlags = hostonlyVMFlags;
    test2.vmScript = dhcpScript;

    headless.virtualisation.virtualbox.headless = true;
    headless.services.xserver.enable = false;
  };

  vboxVMsWithExtpack = mapAttrs createVM {
    testExtensionPack.vmFlags = enableExtensionPackVMFlags;
  };

  mkVBoxTest =
    vboxHostConfig: vms: name: testScript:
    makeTest {
      name = "virtualbox-${name}";

      nodes.machine =
        { lib, config, ... }:
        {
          imports =
            let
              mkVMConf = name: val: val.machine // { key = "${name}-config"; };
              vmConfigs = mapAttrsToList mkVMConf vms;
            in
            [
              ./common/user-account.nix
              ./common/x11.nix
            ]
            ++ vmConfigs;
          virtualisation.memorySize = 2048;

          virtualisation.qemu.options =
            let
              # IvyBridge is reasonably ancient to be compatible with recent
              # Intel/AMD hosts and sufficient for the KVM flavor.
              guestCpu = if config.virtualisation.virtualbox.host.enableKvm then "IvyBridge" else "kvm64";
            in
            [
              "-cpu"
              "${guestCpu},svm=on,vmx=on"
            ];

          test-support.displayManager.auto.user = "alice";
          users.users.alice.extraGroups =
            let
              inherit (config.virtualisation.virtualbox.host) enableHardening;
            in
            lib.mkIf enableHardening [ "vboxusers" ];

          virtualisation.virtualbox.host = {
            enable = true;
          }
          // vboxHostConfig;
        };

      testScript = ''
        from shlex import quote
        ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vms)}

        def ru(cmd: str) -> str:
            return f"su - alice -c {quote(cmd)}"


        def vbm(cmd: str) -> str:
            return machine.succeed(ru(f"VBoxManage {cmd}"))


        def remove_uuids(output: str) -> str:
            return "\n".join(
                [line for line in (output or "").splitlines() if not line.startswith("UUID:")]
            )


        machine.wait_for_x()

        ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"}

        ${testScript}
        # (keep black happy)
      '';

      meta = with pkgs.lib.maintainers; {
        maintainers = [ aszlig ];
      };
    };

  unfreeTests = mapAttrs (mkVBoxTest { enableExtensionPack = true; } vboxVMsWithExtpack) {
    enable-extension-pack = ''
      create_vm_testExtensionPack()
      vbm("startvm testExtensionPack")
      wait_for_startup_testExtensionPack()
      machine.screenshot("cli_started")
      wait_for_vm_boot_testExtensionPack()
      machine.screenshot("cli_booted")

      with machine.nested("Checking for privilege escalation"):
          machine.fail("test -e '/root/VirtualBox VMs'")
          machine.fail("test -e '/root/.config/VirtualBox'")
          machine.succeed("test -e '/home/alice/VirtualBox VMs'")

      shutdown_vm_testExtensionPack()
      destroy_vm_testExtensionPack()
    '';
  };

  kvmTests =
    mapAttrs
      (mkVBoxTest {
        enableKvm = true;

        # Once the KVM version supports these, we can enable them.
        addNetworkInterface = false;
        enableHardening = false;
      } vboxVMs)
      {
        kvm-headless = ''
          create_vm_headless()
          machine.succeed(ru("VBoxHeadless --startvm headless >&2 & disown %1"))
          wait_for_startup_headless()
          wait_for_vm_boot_headless()
          shutdown_vm_headless()
          destroy_vm_headless()
        '';
      };

in
mapAttrs (mkVBoxTest { } vboxVMs) {
  simple-gui = ''
    # Home to select Tools, down to move to the VM, enter to start it.
    def send_vm_startup():
        machine.send_key("home")
        machine.send_key("down")
        machine.send_key("ret")


    create_vm_simple()
    machine.succeed(ru("VirtualBox >&2 &"))
    machine.wait_until_succeeds(ru("xprop -name 'Oracle VirtualBox Manager'"))
    machine.sleep(5)
    machine.screenshot("gui_manager_started")
    send_vm_startup()
    machine.screenshot("gui_manager_sent_startup")
    wait_for_startup_simple(send_vm_startup)
    machine.screenshot("gui_started")
    wait_for_vm_boot_simple()
    machine.screenshot("gui_booted")
    shutdown_vm_simple()
    machine.sleep(5)
    machine.screenshot("gui_stopped")
    machine.send_key("ctrl-q")
    machine.sleep(5)
    machine.screenshot("gui_manager_stopped")
    destroy_vm_simple()
  '';

  simple-cli = ''
    create_vm_simple()
    vbm("startvm simple")
    wait_for_startup_simple()
    machine.screenshot("cli_started")
    wait_for_vm_boot_simple()
    machine.screenshot("cli_booted")

    with machine.nested("Checking for privilege escalation"):
        machine.fail("test -e '/root/VirtualBox VMs'")
        machine.fail("test -e '/root/.config/VirtualBox'")
        machine.succeed("test -e '/home/alice/VirtualBox VMs'")

    shutdown_vm_simple()
    destroy_vm_simple()
  '';

  headless = ''
    create_vm_headless()
    machine.succeed(ru("VBoxHeadless --startvm headless >&2 & disown %1"))
    wait_for_startup_headless()
    wait_for_vm_boot_headless()
    shutdown_vm_headless()
    destroy_vm_headless()
  '';

  host-usb-permissions = ''
    import sys

    user_usb = remove_uuids(vbm("list usbhost"))
    print(user_usb, file=sys.stderr)
    root_usb = remove_uuids(machine.succeed("VBoxManage list usbhost"))
    print(root_usb, file=sys.stderr)

    if user_usb != root_usb:
        raise Exception("USB host devices differ for root and normal user")
    if "<none>" in user_usb:
        raise Exception("No USB host devices found")
  '';

  systemd-detect-virt = ''
    create_vm_detectvirt()
    vbm("startvm detectvirt")
    wait_for_startup_detectvirt()
    wait_for_vm_boot_detectvirt()
    shutdown_vm_detectvirt()
    result = machine.succeed(f"cat '{detectvirt_sharepath}/result'").strip()
    destroy_vm_detectvirt()
    if result != "oracle":
        raise Exception(f'systemd-detect-virt returned "{result}" instead of "oracle"')
  '';

  net-hostonlyif = ''
    create_vm_test1()
    create_vm_test2()

    vbm("startvm test1")
    wait_for_startup_test1()
    wait_for_vm_boot_test1()

    vbm("startvm test2")
    wait_for_startup_test2()
    wait_for_vm_boot_test2()

    machine.screenshot("net_booted")

    test1_ip = wait_for_ip_test1(1)
    test2_ip = wait_for_ip_test2(1)

    machine.succeed(f"echo '{test2_ip}' | nc -N '{test1_ip}' 1234")
    machine.succeed(f"echo '{test1_ip}' | nc -N '{test2_ip}' 1234")

    machine.wait_until_succeeds(f"nc -N '{test1_ip}' 5678 < /dev/null >&2")
    machine.wait_until_succeeds(f"nc -N '{test2_ip}' 5678 < /dev/null >&2")

    shutdown_vm_test1()
    shutdown_vm_test2()

    destroy_vm_test1()
    destroy_vm_test2()
  '';
}
// (optionalAttrs enableKvm kvmTests)
// (optionalAttrs enableUnfree unfreeTests)
